package com.nulldev.util.networking.async2.tcp.client.connectors;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Arrays;

import com.nulldev.util.networking.async2.DataFrame;
import com.nulldev.util.networking.async2.tcp.client.Async2TCPClient;
import com.nulldev.util.networking.async2.tcp.client.exceptions.ConnectorCannotSendPacketException;
import com.nulldev.util.networking.async2.tcp.client.exceptions.ConnectorDisconnectedException;
import com.nulldev.util.networking.async2.tcp.client.packets.IncomingTCPClientPacket;
import com.nulldev.util.networking.async2.tcp.client.packets.MessageCollectorTCPClientPacket;
import com.nulldev.util.networking.async2.tcp.client.packets.OutgoingTCPClientPacket;

public class TCPConnector {
	public Async2TCPClient fMyTCPClient;
	private final String fName;
	private final InetAddress fServerIP;
	private final int fServerPort;
	private boolean fActive;
	protected boolean fIsConnected = false;
	// Socket properties.
	public Socket fConnection;
	private final int fReadBufferSize;
	public InputStream fInput;
	public OutputStream fOutput;
	protected final Object fSendDataLock = new Object();
	protected final Object fReceiveDataLock = new Object();
	private ReadIncomingDataThread fReadIncomingDataThread;
	// Data throughput.
	public long fBytesIn = 0, fBytesOut = 0;
	// Message collector properties.
	private boolean fUseMessageCollector;
	private String fETX;
	public TCPClientMessageCollector fMessageCollector;
	// Messages IO.
	public IncomingTCPClientPacket fLastIncomingPacket = null;
	public MessageCollectorTCPClientPacket fLastMessageCollectorPacket = null;
	private OutgoingTCPClientPacket fLastOutgoingPacket = null;

	/**
	 * Constructs a new TCP Connector.
	 *
	 * @param myTCPClient    is the TCP connector's main Extasys TCP Client.
	 * @param name           is the connector's name.
	 * @param serverIP       is the server's ip address the connector will use to
	 *                       connect.
	 * @param serverPort     is the server's tcp port the connector will use to
	 *                       connect.
	 * @param readBufferSize is the read buffer size in bytes for this connection.
	 */
	public TCPConnector(Async2TCPClient myTCPClient, String name, InetAddress serverIP, int serverPort, int readBufferSize) {
		fMyTCPClient = myTCPClient;
		fName = name;
		fServerIP = serverIP;
		fServerPort = serverPort;
		fReadBufferSize = readBufferSize;
	}

	/**
	 * Constructs a new TCP Connector with message collector use (ETX).
	 *
	 * @param myTCPClient    is the TCP connector's main Extasys TCP Client.
	 * @param name           is the connector's name.
	 * @param serverIP       is the server's ip address the connector will connect
	 *                       to.
	 * @param serverPort     is the server's tcp port the connector will connect to.
	 * @param readBufferSize is the read buffer size in bytes for this connection.
	 * @param ETX            is the End of Text character.
	 */
	public TCPConnector(Async2TCPClient myTCPClient, String name, InetAddress serverIP, int serverPort, int readBufferSize, char ETX) {
		fMyTCPClient = myTCPClient;
		fName = name;
		fServerIP = serverIP;
		fServerPort = serverPort;
		fReadBufferSize = readBufferSize;

		fUseMessageCollector = true;
		fETX = String.valueOf(ETX);
		fMessageCollector = new TCPClientMessageCollector(this, ETX);
	}

	/**
	 * Constructs a new TCP Connector with message collector use (Splitter).
	 *
	 * @param myTCPClient    is the TCP connector's main Extasys TCP Client.
	 * @param name           is the connector's name.
	 * @param serverIP       is the server's ip address the connector will connect
	 *                       to.
	 * @param serverPort     is the server's tcp port the connector will connect to.
	 * @param readBufferSize is the read buffer size in bytes for this connection.
	 * @param splitter       is the message splitter.
	 */
	public TCPConnector(Async2TCPClient myTCPClient, String name, InetAddress serverIP, int serverPort, int readBufferSize, String splitter) {
		fMyTCPClient = myTCPClient;
		fName = name;
		fServerIP = serverIP;
		fServerPort = serverPort;
		fReadBufferSize = readBufferSize;

		fUseMessageCollector = true;
		fETX = splitter;
		fMessageCollector = new TCPClientMessageCollector(this, splitter);
	}

	/**
	 * Start the connector (connect to the server).
	 */
	public void start() throws Exception {
		if (!fActive) {
			fActive = true;
			try {
				fConnection = new Socket(fServerIP, fServerPort);
				fInput = fConnection.getInputStream();
				fOutput = fConnection.getOutputStream();
			} catch (Exception ex) {
				stop();
				throw new Exception(ex.getMessage());
			}

			fLastIncomingPacket = null;
			fLastMessageCollectorPacket = null;
			fLastOutgoingPacket = null;

			try {
				fReadIncomingDataThread = new ReadIncomingDataThread(this);
				fReadIncomingDataThread.start();
			} catch (Exception ex) {
				stop();
				throw new Exception(ex.getMessage());
			}
		}
	}

	/**
	 * Stop the connector (disconnect from the server).
	 */
	public void stop() {
		stop(false);
	}

	/**
	 * Force connector stop (disconnect from the server).
	 */
	public void forceStop() {
		stop(true);
	}

	private void stop(boolean force) {
		if (fActive) {
			if (!force) {
				// Wait to process incoming and outgoing TCP Packets
				if (fLastMessageCollectorPacket != null) {
					fLastMessageCollectorPacket.fDone.waitOne();
				} else if (fLastIncomingPacket != null) {
					fLastIncomingPacket.fDone.waitOne();
				}

				if (fLastOutgoingPacket != null) {
					fLastOutgoingPacket.fDone.waitOne();
				}
			}

			try {
				if (fConnection != null) {
					fConnection.close();
				}
			} catch (Exception ex) {
			}

			try {
				if (fInput != null) {
					fInput.close();
				}
			} catch (Exception ex) {
			}
			fInput = null;

			try {
				if (fOutput != null) {
					fOutput.close();
				}
			} catch (Exception ex) {
			}
			fOutput = null;

			if (fLastIncomingPacket != null) {
				fLastIncomingPacket.cancel();
			}

			if (fLastMessageCollectorPacket != null) {
				fLastMessageCollectorPacket.cancel();
			}

			if (fLastOutgoingPacket != null) {
				fLastOutgoingPacket.cancel();
			}

			fActive = false;
			fIsConnected = false;

			if (fReadIncomingDataThread != null) {
				fReadIncomingDataThread.dispose();
			}

			if (fUseMessageCollector) {
				fMessageCollector.dispose();
				fMessageCollector = null;
			}

			try {
				fMyTCPClient.onDisconnect(this);
			} catch (Exception ex) {
			}
			fConnection = null;
		}
	}

	/**
	 * Send string data to server.
	 *
	 * @param data is the string to send.
	 */
	public void sendData(String data) throws ConnectorDisconnectedException, ConnectorCannotSendPacketException {
		try {
			byte[] bytes = data.getBytes();
			sendData(bytes, 0, bytes.length);
		} catch (Exception ex) {
			throw new ConnectorCannotSendPacketException(this, null);
		}
	}

	/**
	 * Send data to server.
	 *
	 * @param bytes  is the byte array to be send.
	 * @param offset is the position in the data buffer at witch to begin sending.
	 * @param length is the number of the bytes to be send.
	 */
	public void sendData(byte[] bytes, int offset, int length) throws ConnectorDisconnectedException, ConnectorCannotSendPacketException {
		synchronized (fSendDataLock) {
			if (fIsConnected) {
				fLastOutgoingPacket = new OutgoingTCPClientPacket(this, bytes, offset, length, fLastOutgoingPacket);
			} else {
				throw new ConnectorDisconnectedException(this);
			}
		}
	}

	/**
	 * Send data to server and wait until data transfer complete.
	 *
	 * @param data is the string data to send
	 * @throws ConnectorDisconnectedException
	 */
	public void sendDataSynchronous(String data) throws ConnectorDisconnectedException {
		byte[] bytes = data.getBytes();
		sendDataSynchronous(bytes, 0, bytes.length);
	}

	/**
	 * Send data to server and wait until data transfer complete.
	 *
	 * @param bytes  is the byte array to be send.
	 * @param offset is the position in the data buffer at witch to begin sending.
	 * @param length is the number of the bytes to be send.
	 * @throws ConnectorDisconnectedException
	 */
	public void sendDataSynchronous(byte[] bytes, int offset, int length) throws ConnectorDisconnectedException {
		try {
			fOutput.write(bytes, offset, length);
			fBytesOut += length;
		} catch (IOException ex) {
			stop();
			throw new ConnectorDisconnectedException(this);
		}
	}

	/**
	 * Returns the main TCP Client of the connector.
	 *
	 * @return the main TCP Client of the connector.
	 */
	public Async2TCPClient getTCPClient() {
		return fMyTCPClient;
	}

	/**
	 * Returns the active state of this connector.
	 *
	 * @return True if connector is active and connected.
	 */
	public boolean isActive() {
		return fActive;
	}

	/**
	 * Returns the name of this connector.
	 *
	 * @return the name of this connector
	 */
	public String getName() {
		return fName;
	}

	/**
	 * Returns the remote server's ip address.
	 *
	 * @return the remote server's ip address
	 */
	public InetAddress getServerIPAddress() {
		return fServerIP;
	}

	/**
	 * Returns the remote server TCP port.
	 *
	 * @return the remote server TCP port.
	 */
	public int getServerPort() {
		return fServerPort;
	}

	/**
	 * Returns the read buffer size of the connection.
	 *
	 * @return the read buffer size of the connection
	 */
	public int getReadBufferSize() {
		return fReadBufferSize;
	}

	/**
	 * Return the number of bytes received from this connector.
	 *
	 * @return the number of bytes received from this connector.
	 */
	public long getBytesIn() {
		return fBytesIn;
	}

	/**
	 * Returns the number of bytes send from this connector.
	 *
	 * @return the number of bytes send from this connector.
	 */
	public long getBytesOut() {
		return fBytesOut;
	}

	/**
	 * Returns the active state of the message collector.
	 *
	 * @return True if this connector uses message collector.
	 */
	public boolean isMessageCollectorInUse() {
		return fUseMessageCollector;
	}

	/**
	 * Returns the message collector of this connector.
	 *
	 * @return the message collector of this connector.
	 */
	public TCPClientMessageCollector getMyMessageCollector() {
		return fMessageCollector;
	}

	/**
	 * Returns message collector's splitter in string format.
	 *
	 * @return the message collector's splitter in string format.
	 */
	public String getMessageSplitter() {
		return fETX;
	}

	/**
	 * Returns True if this connector is connected to the host.
	 *
	 * @return True if this connector is connected to the host.
	 */
	public boolean isConnected() {
		return fIsConnected;
	}
}

/**
 * This thread reads the incoming data.
 *
 * @author Nikos Siatras
 */
class ReadIncomingDataThread extends Thread {

	private TCPConnector fMyTCPConnector;
	private byte[] fReadBuffer;
	private boolean fActive = false;

	public ReadIncomingDataThread(TCPConnector myTCPConnector) {
		fMyTCPConnector = myTCPConnector;
		fReadBuffer = new byte[myTCPConnector.getReadBufferSize()];

		if (!fMyTCPConnector.fIsConnected) {
			fMyTCPConnector.fIsConnected = true;
			fMyTCPConnector.getTCPClient().onConnect(fMyTCPConnector);
		}
	}

	@Override
	public void start() {
		fActive = true;
		super.start();
	}

	public void dispose() {
		fActive = false;
		try {
			this.interrupt();
		} catch (Exception ex) {
		}
	}

	@Override
	public void run() {
		int bytesRead = 0;

		while (fActive) {
			try {
				bytesRead = fMyTCPConnector.fInput.read(fReadBuffer);
				fMyTCPConnector.fBytesIn += bytesRead;
				if (bytesRead > 0) {
					if (!fMyTCPConnector.isMessageCollectorInUse()) // No message collector.
					{
						try {
							synchronized (fMyTCPConnector.fReceiveDataLock) {
								fMyTCPConnector.fLastIncomingPacket = new IncomingTCPClientPacket(fMyTCPConnector, new DataFrame(fReadBuffer, 0, bytesRead),
										fMyTCPConnector.fLastIncomingPacket);
							}
						} catch (Exception ex) {
							fMyTCPConnector.stop();
						}
					} else // Message collector is enabled.
					{
						try {
							synchronized (fMyTCPConnector.fReceiveDataLock) {
								fMyTCPConnector.fLastMessageCollectorPacket = new MessageCollectorTCPClientPacket(fMyTCPConnector,
										Arrays.copyOfRange(fReadBuffer, 0, bytesRead), fMyTCPConnector.fLastMessageCollectorPacket);
							}
						} catch (Exception ex) {
							fMyTCPConnector.stop();
						}
					}
				} else {
					fMyTCPConnector.stop();
				}
			} catch (IOException ex) {
				fMyTCPConnector.stop();
			} catch (Exception ex) {
			}
		}
	}
}
